package simulator.eventHandler;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;

import simulator.Configuration;
import simulator.Event;
import simulator.EventHandler;
import simulator.Metadata;
import simulator.Result;
import simulator.Unit;
import simulator.unit.*;

public class RandomDistributeEventHandler implements EventHandler{
	
	private final int replicaCount = 3;
	private byte []availableCount = null;
	private byte []durableCount = null;
	
	private int unavailableSliceCount = 0;
	private int unDurableSliceCount = 0;
	
	public Result end() {
		
		Result ret = new Result();
		ret.unAvailableCount = this.unavailableSliceCount;
		ret.unDurableCount = this.unDurableSliceCount;
		return ret;
	}

	public void handleEvent(Event e) {
		if(e.getType()==Event.EventType.Failure){
			handleFailure(e.getUnit(), e.getTime());
		}
		else if(e.getType()==Event.EventType.Recovered){
			handleRecovery(e.getUnit(), e.getTime());
		}
		
	}
	
	private void handleFailure(Unit u, double time){
		if(u instanceof Machine){
			for(Integer sliceIndex:u.getMetadata().slices){
				availableCount[sliceIndex]--;
				assert(availableCount[sliceIndex]>=0);
				if(availableCount[sliceIndex]==0){
					//System.out.println("slice "+sliceIndex+" becomes unavailable at time "+time);
					unavailableSliceCount++;
				}
				
			}
		}
		else if(u instanceof Raid){
			for(Integer sliceIndex:u.getParent().getMetadata().slices){
				durableCount[sliceIndex]--;
				assert(durableCount[sliceIndex]>=0);
				if(durableCount[sliceIndex]==0){
					//System.out.println("slice "+sliceIndex+" becomes undurable at time "+time);
					unDurableSliceCount++;
				}
				
			}
		}
		else{
			for(Unit tmp:u.getChildren())
				handleFailure(tmp, time);
		}
	}
	
	private void handleRecovery(Unit u, double time){
		if(u instanceof Machine){
			for(Integer sliceIndex:u.getMetadata().slices){
				//if(availableCount[sliceIndex]==0)
				//	System.out.println("slice "+sliceIndex+" recovered at "+time);
				availableCount[sliceIndex]++;
				assert(availableCount[sliceIndex]<=replicaCount);	
			}
		}
		else{
			for(Unit tmp:u.getChildren())
				handleRecovery(tmp, time);
		}
	}

	public void start(Unit root) {
		distributeSlices(root);
	}
	
	private void distributeSlices(Unit root){
		int machineCount = getMachineCount(root);
		int totalSlices = machineCount*Configuration.slicesPerMachine/replicaCount*5/6;
		System.out.println("totalSlices="+totalSlices);
		availableCount = new byte[totalSlices];
		durableCount = new byte[totalSlices];
		ArrayList<Unit> machines = new ArrayList<Unit>(machineCount);
		getAllMachines(root, machines);
		LinkedList<Unit> tmp = new LinkedList<Unit>();
		for(int i=0; i<totalSlices; i++){
			tmp.clear();
			for(int j=0; j<replicaCount; j++){
				distributeOneSliceToOneMachine(i, tmp, machines);
			}
			assert(tmp.size()==replicaCount);
			availableCount[i] = replicaCount;
			durableCount[i] = replicaCount;
		}
	}
	
	private Random r = new Random();
	private void distributeOneSliceToOneMachine(int sliceIndex, LinkedList<Unit> previous, ArrayList<Unit> machines){
		int retryCount=0;
		while(true){
			retryCount++;
			boolean success = true;
			int machineIndex = r.nextInt(machines.size());
			Unit machine = machines.get(machineIndex);
			
			//machine cannot be overloaded
			if(machine.getMetadata().sliceCount>=Configuration.slicesPerMachine){
				success=false;
				continue;
			}
			for(Unit u:previous){
				//Two copies cannot be on the same rack
				if(u.getParent()==machine.getParent()){
					success=false;
					break;
				}
			}
			if(success){
				Metadata m = machine.getMetadata();
				if(m.slices==null)
					m.slices = new int[Configuration.slicesPerMachine];
				m.slices[m.sliceCount]=sliceIndex;
				m.sliceCount++;
				previous.add(machine);
				return;
			}
			if(retryCount>100){
				System.out.println("Unable to distribute slice "+sliceIndex);
				return;
			}
			
		}
	}
	
	private int getDiskCount(Unit u){
		int ret = 0;
		for(Unit tmp:u.getChildren()){
			if(tmp instanceof Disk)
				ret++;
			else
				ret+=getDiskCount(tmp);
		}
		return ret;
	}
	
	private int getMachineCount(Unit u){
		int ret = 0;
		for(Unit tmp:u.getChildren()){
			if(tmp instanceof Machine)
				ret++;
			else
				ret+=getMachineCount(tmp);
		}
		return ret;
	}
	
	private void getAllMachines(Unit u, ArrayList<Unit> machines){
		for(Unit tmp:u.getChildren()){
			if(tmp instanceof Machine)
				machines.add(tmp);
			else
				getAllMachines(tmp, machines);
		}
	}

}
